/*
 * $Id: TargetManager.java 3972 2011-03-17 20:31:58Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.action;

import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JComponent;

/**
 * The target manager dispatches commands to {@link Targetable} objects that it
 * manages. This design of this class is based on the <i>Chain of
 * Responsiblity</i> and <i>Mediator</i> design patterns. The target manager
 * acts as a mediator between {@link TargetableAction}s and the intended
 * targets. This allows Action based components to invoke commands on components
 * without explicitly binding the user Action to the component action.
 * <p>
 * The target manager maintains a reference to a current target and a target
 * list. The target list is managed using the <code>addTarget</code> and
 * <code>removeTarget</code> methods. The current target is managed using the
 * <code>setTarget</code> and <code>getTarget</code> methods.
 * <p>
 * Commands are dispatched to the Targetable objects in the
 * <code>doCommand</code> method in a well defined order. The doCommand method
 * on the Targetable object is called and if it returns true then the command
 * has been handled and command dispatching will stop. If the Targetable
 * doCommand method returns false then the
 * <p>
 * If none of the Targetable objects can handle the command then the default
 * behaviour is to retrieve an Action from the {@link javax.swing.ActionMap} of
 * the permanent focus owner with a key that matches the command key. If an
 * Action can be found then the <code>actionPerformed</code> method is invoked
 * using an <code>ActionEvent</code> that was constructed using the command
 * string.
 * <p>
 * If the Action is not found on the focus order then the ActionMaps of the
 * ancestor hierarchy of the focus owner is searched until a matching Action can
 * be found. Finally, if none of the components can handle the command then it
 * is dispatched to the ActionMap of the current Application instance.
 * <p>
 * The order of command dispatch is as follows:
 * <ul>
 * <li>Current Targetable object invoking doCommand method
 * <li>List order of Targetable objects invoking doCommand method
 * <li>ActionMap entry of the permanent focus owner invoking actionPerfomed
 * <li>ActionMap entry of the ancestor hierarchy of the permanent focus owner
 * <li>ActionMap entry of the current Application instance
 * </ul>
 *
 * @see Targetable
 * @see TargetableAction
 * @author Mark Davidson
 */
public class TargetManager {

	private static TargetManager INSTANCE;
	private List<Targetable> targetList;
	private Targetable target;
	private PropertyChangeSupport propertySupport;

	/**
	 * Create a target manager. Use this constructor if the application may
	 * support many target managers. Otherwise, using the getInstance method
	 * will return a singleton.
	 */
	public TargetManager() {
		propertySupport = new PropertyChangeSupport(this);
	}

	/**
	 * Return the singleton instance.
	 */
	public static TargetManager getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new TargetManager();
		}
		return INSTANCE;
	}

	/**
	 * Add a target to the target list. Will be appended to the list by default.
	 * If the prepend flag is true then the target will be added at the head of
	 * the list.
	 * <p>
	 * Targets added to the head of the list will will be the first to handle
	 * the command.
	 *
	 * @param target
	 *            the targeted object to add
	 * @param prepend
	 *            if true add at the head of the list; false append
	 */
	public void addTarget(Targetable target, boolean prepend) {
		if (targetList == null) {
			targetList = new ArrayList<Targetable>();
		}
		if (prepend) {
			targetList.add(0, target);
		} else {
			targetList.add(target);
		}
		// Should add focus listener to the component.
	}

	/**
	 * Appends the target to the target list.
	 * 
	 * @param target
	 *            the targeted object to add
	 */
	public void addTarget(Targetable target) {
		addTarget(target, false);
	}

	/**
	 * Remove the target from the list
	 */
	public void removeTarget(Targetable target) {
		if (targetList != null) {
			targetList.remove(target);
		}
	}

	/**
	 * Returns an array of managed targets that were added with the
	 * <code>addTarget</code> methods.
	 *
	 * @return all the <code>Targetable</code> added or an empty array if no
	 *         targets have been added
	 */
	public Targetable[] getTargets() {
		Targetable[] targets;
		if (targetList == null) {
			targets = new Targetable[0];
		} else {
			targets = new Targetable[targetList.size()];
			targets = (Targetable[]) targetList.toArray(new Targetable[targetList.size()]);
		}
		return targets;
	}

	/**
	 * Gets the current targetable component. May or may not in the target list.
	 * If the current target is null then the the current targetable component
	 * will be the first one in the target list which can execute the command.
	 *
	 * This is a bound property and will fire a property change event if the
	 * value changes.
	 *
	 * @param newTarget
	 *            the current targetable component to set or null if the
	 *            TargetManager shouldn't have a current targetable component.
	 */
	public void setTarget(Targetable newTarget) {
		Targetable oldTarget = target;
		if (oldTarget != newTarget) {
			target = newTarget;
			propertySupport.firePropertyChange("target", oldTarget, newTarget);
		}
	}

	/**
	 * Return the current targetable component. The curent targetable component
	 * is the first place where commands will be dispatched.
	 *
	 * @return the current targetable component or null
	 */
	public Targetable getTarget() {
		return target;
	}

	public void addPropertyChangeListener(PropertyChangeListener listener) {
		propertySupport.addPropertyChangeListener(listener);
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		propertySupport.removePropertyChangeListener(listener);
	}

	/**
	 * Executes the command on the current targetable component. If there isn't
	 * current targetable component then the list of targetable components are
	 * searched and the first component which can execute the command. If none
	 * of the targetable components handle the command then the ActionMaps of
	 * the focused components are searched.
	 *
	 * @param command
	 *            the key of the command
	 * @param value
	 *            the value of the command; depends on context
	 * @return true if the command has been handled otherwise false
	 */
	public boolean doCommand(Object command, Object value) {
		// Try to invoked the explicit target.
		if (target != null) {
			if (target.hasCommand(command) && target.doCommand(command, value)) {
				return true;
			}
		}

		// The target list has the next chance to handle the command.
		if (targetList != null) {
			Iterator<Targetable> iter = targetList.iterator();
			while (iter.hasNext()) {
				Targetable target = iter.next();
				if (target.hasCommand(command) && target.doCommand(command, value)) {
					return true;
				}
			}
		}

		ActionEvent evt = null;
		if (value instanceof ActionEvent) {
			evt = (ActionEvent) value;
		}

		// Fall back behavior. Get the component which has focus and search the
		// ActionMaps in the containment hierarchy for matching action.
		Component comp = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
		while (comp != null) {
			if (comp instanceof JComponent) {
				ActionMap map = ((JComponent) comp).getActionMap();
				Action action = map.get(command);
				if (action != null) {
					if (evt == null) {
						evt = new ActionEvent(comp, 0, command.toString());
					}
					action.actionPerformed(evt);

					return true;
				}
			}
			comp = comp.getParent();
		}

		return false;
	}

	/**
	 * Resets the TargetManager. This method is package private and for testing
	 * purposes only.
	 */
	void reset() {
		if (targetList != null) {
			targetList.clear();
			targetList = null;
		}
		target = null;

		PropertyChangeListener[] listeners = propertySupport.getPropertyChangeListeners();
		for (int i = 0; i < listeners.length; i++) {
			propertySupport.removePropertyChangeListener(listeners[i]);
		}
		INSTANCE = null;
	}

}
